fix(client): organizations.getActiveMember addresses the organisation the caller NAMES, not whichever one the session has active - #16761
Conversation
`organizations.getActiveMember(organizationId)` built `GET /organization/get-active-member?organizationId=...`, and better-auth 1.7.2's handler for that path reads `session.session.activeOrganizationId` and never looks at `ctx.query`. The query string was dead on arrival: a permission check for organisation B while A was active answered A's row, with a 200 and no diagnostic. The method now asks the question honestly, in two requests: `GET /get-session` for the caller's own user id, then `GET /organization/list-members?organizationId=...&filterField=userId&filterValue=<self>&limit=1`, unwrapping the one-entry page. `list-members` reads `ctx.query.organizationId` and its rows carry the identical shape, so the signature and the declared return type are unchanged. The `get-active-member` ledger row is rebooked `server-only`: no SDK method builds that URL any more, and `sdk` means "expressed by the SDK". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
check:doc-authoring — a runtime string reaches authors and generated surfaces, none of whom can resolve `#NNNN`; git history keeps the anchor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
…t-active-member-organization-id
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1f7d99883de0e9812537dc011d8abd0093652f1f && git checkout 1f7d99883de0e9812537dc011d8abd0093652f1f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f6b7c53db7b65bbfb019750efb4e545470b0c2b7 f6c694123d7deaf843623d1ccfcefecac651c6c4 && git checkout -B drift-repro f6b7c53db7b65bbfb019750efb4e545470b0c2b7 && git merge --no-ff f6c694123d7deaf843623d1ccfcefecac651c6c4
node scripts/docs-audit/affected-docs.mjs --json f6b7c53db7b65bbfb019750efb4e545470b0c2b7
|
Check Changeset: a PR declaring clause-② yes may not grade a package it grew `patch`. The maintainer's ruling of 2026-09-04 (decision batch #35) holds that a change to a published package's public surface takes at least `minor`; a commit type may raise a bump, never lower it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Contract review (
|
…t-active-member-organization-id
better-auth resolves `ctx.query.organizationId || session.activeOrganizationId` on `list-members`, so an empty string fell through to session state and came back 200 carrying the ACTIVE organisation's row — the same silent substitution this method was fixed to stop making, surviving on one argument while the JSDoc says "the GIVEN organisation". The SDK now refuses it before the wire, in the shape `environment(id)` already uses. The pinned case asserts nothing reaches the wire at all, and drives `list-members` with an empty id through the same double to show the fallback the refusal prevents is real in the fixture, not assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5
… each URL `get-active-member` was rebooked `server-only` because a truth ledger must not ship a false statement; by the same standard two rows were left incomplete. `get-session` named only `auth.me` and `auth.refreshToken`, and `list-members` named only `organizations.listMembers`, while `organizations.getActiveMember` now builds both. The `invite-member` row is the precedent for exactly this. Also restores a by-name anchor for the method: after the rebooking it was pinned by URL through `client-url-conformance.test.ts` but by no `client:` or `note:` string anywhere in the ledger. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5
The changeset now carries the `**BREAKING**` banner, one before/after pair per moved input, and an ADR-0087 `not-required (no-migration-prescription)` disposition. The level stays `minor`: under the launch-window convention the level cannot carry breaking-ness, so the banner and the disposition are the carriers. Four inputs move, each stated as the response it drew before and the response it draws now: an id other than the active organisation; an organisation the caller is not a member of; any id on a session with no active organisation; and an empty id, which this round refuses client-side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018rzQyhLGC5iVs11V3TzRs5
|
Director seat adoption record — summon #20, Contract review (
|
| owed | status | evidence |
|---|---|---|
F1 — ledger notes on get-session and list-members name every SDK method that builds each URL |
done | packages/plugins/plugin-auth/src/auth-route-ledger.ts:158 (auth.me, auth.refreshToken and organizations.getActiveMember all target it …), :262 (organizations.listMembers and organizations.getActiveMember both build it — the latter with filterField=userId&filterValue=<the caller>&limit=1 …). Tracker ids absent from both strings (the check:doc-authoring ruling). |
F2 — refuse a falsy organizationId client-side with a loud error, one pinned case |
done | packages/client/src/index.ts:3483-3485 if (!organizationId) throw new Error('[ObjectStack] organizations.getActiveMember: organizationId is required'), JSDoc @param/@throws at :3472-3476; pinned by case ⑧ organization-get-active-member-addressing.test.ts:276-301, which asserts urls is [] (nothing on the wire) and drives the fallback the guard prevents through the same double (guard-the-guard). |
F3 (ruled) — **BREAKING** banner, FROM→TO per moved input, ADR-0087 disposition, level stays minor |
done | .changeset/client-get-active-member-names-the-organisation.md:2 "@objectstack/client": minor; :7 **BREAKING** — …; :22-25 four before/after bullets (non-active id, non-member, no active org, empty id); :33 <!-- adr-0087: not-required (no-migration-prescription) SDK call-site change, no metadata conversion --> — exactly one marker, parses under the gate's readDisposition regex (scripts/check-adr-0087-registration.mjs:1502), category in the closed set. |
Consolidated seat's patch-round claim 5594619517 |
claims hold | file surface = exactly the 4 paths in the diff; the extra index.ts hunks at ~:430, ~:1058, ~:1150 visible in eb75819..head came in via the main merge 7723332f, not this PR — origin/main...head touches only the getActiveMember region :3428-3513. |
Gates re-run by this seat, read-only, against the review ref: node scripts/check-adr-0087-registration.mjs --base origin/main --head refs/pr-review/16761 → exit 0 ("1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … not-required (no-migration-prescription)"); node scripts/check-changeset-no-major.mjs --base origin/main --head refs/pr-review/16761 --event <live PR payload> → exit 0 ("no major bump"; "LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch · carrier IS on this PR · declaration line: Clause-②: yes").
Derived judgments
- Published client signature — unchanged.
getActiveMember: async (organizationId: string): Promise<OrganizationMemberWithUserWire>(index.ts:3479), byte-identical tomain;return-type-precision.test.ts:950still pins the return type; no export added or removed. - Wire behaviour — now two requests, addressing honest.
GET {auth}/get-session(withOrigin, the same shapeauth.meuses at:3989) →GET {auth}/organization/list-members?organizationId=<enc>&filterField=userId&filterValue=<enc self>&limit=1→page.members[0], else a loud throw (:3508-3511). The dead routeget-active-memberis no longer built by any SDK method (git grepat head: only ledger prose, tests and the changeset name it). - Vendor premise — confirmed at the pinned version from source, not from the PR.
packages/plugins/plugin-auth/package.json:40pins"better-auth": "1.7.2"; in that tarball'sdist/plugins/organization/routes/crud-members.mjs:getActiveMemberreadssession.session.activeOrganizationIdonly and neverctx.query(400NO_ACTIVE_ORGANIZATION/ 400MEMBER_NOT_FOUND/ 200 row);listMembersresolvesctx.query?.organizationId || session.session.activeOrganizationId, thenfindMemberByOrgId({ userId: session.user.id, organizationId })→FORBIDDEN YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATIONbefore any filter;limitschema isz.string().or(z.number()).optional()coerced withNumber(...),filterField/filterValueare free strings → the query the SDK sends is accepted as-is.get-sessionanswersctx.json(null)for an anonymous caller (api/routes/session.mjs:157);orgSessionMiddlewarewrapssessionMiddleware(call.mjs:11) → 401 on the second request, server-thrown, nothing client-invented. The runtime mount (packages/runtime/src/domains/auth.ts:138) returns the auth service'sResponsewith its body untouched, so the bare{ user, session }/nullthe SDK types inline is what a deployed server serves. - Security — no authorization widening. Read authorised by the caller's own membership in the named organisation, not by session state;
filterValueis fixed to the caller's own id; an arbitrary filter would expose no more thanorganizations.listMembers(same route, alreadysdk) does today. - Server side — unchanged.
packages/restuntouched;plugin-authmoves onlyauth-route-ledger.ts(three rows). That module has zero non-test importers at head and is not atsupentry (tsup.config.ts:40:['src/index.ts', 'src/rate-limit-storage.ts']), so nothing published moves in@objectstack/plugin-authand no changeset is owed there. Rebookingget-active-membertoserver-only("Deliberately not SDK surface", type doc at ledger:60) is the right word —gapwould assert the SDK should call a route that cannot answer the question — and the non-sdknotethe conformance test demands (auth-route-ledger.conformance.test.ts:164) is present. - Spec contracts — no
packages/specpath;OrganizationMemberWithUserWire/OrganizationMembersPage(index.ts:1265,:1368) unchanged. - Tests — 8 cases, no
.skip/.only/.todo, undertsconfig.test.json'ssrc/**/*. ① is the revert-reddening value pin, ⑦/⑧ are guard-the-guard legs, ⑤ pins the ADR-0112 envelope (code,httpStatus), ⑥ pins the anonymous 401 with two URLs on the wire.client-url-conformance.test.ts:389-393catches a throw after the request, so with its placeholder body the method records both URLs and both matchsdkrows. - Clause-②:
yes— right. Mechanical floor clean (no new symbol, key, signature or type). But per.claude/skills/pm-dispatch/references/contract-review.md:14-15"在两个已发布码之间重选输入类" is judgement, and here the input classes reaching400 MEMBER_NOT_FOUND/403 YOU_ARE_NOT_A_MEMBER…/400 NO_ACTIVE_ORGANIZATION/ 200 are re-chosen, and the empty-id class now goes to a client throw. PR body line 2 is the literalClause-②: yes; the governing claim5594619517saysyes;needs:contract-reviewis on both carriers (PR labels and card labels read at review time).
Semver / changeset
@objectstack/client: minor— correct: clause-②yes+packages/client/src/**moved ⇒ ≥minor(level-axis, [finding] No gate answers whether a changeset's LEVEL fits the surface — Check Changeset is green on patch and on minor for the same diff #16055);patchwas the red at012d430b, fixed ateb75819. Launch-window rule:majoris refused, so the level cannot carry breaking-ness.**BREAKING**banner present (:7), required by ruling5580367898. The gate's only carriers during the window are the banner and the ADR-0087 disposition — both present.- ADR-0087
not-required (no-migration-prescription): on the merits, not only on detector silence — ADR-0087 registers metadata conversions (objectstack migrate meta,spec-changes.json), and this diff converts no metadata;runtime-interface-onlyis closed (dotted member path, and the change is behavioural),type-surface-onlydoes not apply.findMigrationPrescriptionreturns null on the body (gate exit 0 offline and in CICheck Changeset, the job that hosts both steps, success 03:29Z on this head). - No
@objectstack/plugin-authentry — correct (item 5).
Boundary flags
- Governed paths: none. The four files hit none of
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md,content/docs/releases/**(register inscripts/pm/check-governed-merges.mjs; CI "Governed Surface Queue Guard" success). Lands through the queue after the owning seat clears the carriers; not maintainer-by-hand. - Dev's latest
os-dev-reportis5579200601(2026-09-08T04:26Z, oneb75819):open_questions: [];deviations: [FOOTER]— a PR-body attribution-footer duplication, corrected by read-back; no contract effect, accepted. The delivery report5578965259:open_questions: []; deviations SCOPE (ledger row — accepted, verified in item 5), CHANNEL / RESOURCE / BASE (tooling, no contract bearing — noted), MEASUREMENT SITE (probe not shipped — accepted: this seat read the vendor handlers independently, item 3). - No
os-dev-reportexists for the patch round (d3ddfa11..4ebf8692): the dev was killed by a 429 before the gate union and the report (5597048143, dead-claim recovery; card nowpm:queue, no assignee). See F1. - Sibling
objectuicalls better-auth's ownorganization.getActiveMember(packages/auth/src/createAuthClient.ts:850), not the ObjectStack SDK → no downstream consumer in the sibling moves.content/docsnamesgetActiveMembernowhere; the drift check's one row (permissions/authentication.mdx, viaget-session) lists the route as a route only — accurate. - Head is 34 behind / 9 ahead of
origin/main; none of the 34 touch the PR's four files;git merge-tree --write-treeclean. GitHub reportedmergeable_state: unknownat read time (not yet recomputed) — a dry merge says clean.
Findings
F1 — non-blocking (process): the patch-round increment has no os-dev-report. d3ddfa11, b4dc6258, 4ebf8692 were pushed, the PR body was updated, and then the dev died (5597048143). CI on the head is green end-to-end and this seat re-ran both changeset gates offline, so the contract surface is covered; the report is still owed by whoever re-claims (card is pm:queue, unassigned). Expectation: the next dispatcher posts it per the resume shape in 5597048143; no code change.
F2 — non-blocking (wording accuracy in shipped CHANGELOG text): the "before" on the non-member bullet is over-stated. .changeset/…names-the-organisation.md:23 and packages/client/src/index.ts:3456-3457 say a non-member of the named organisation was answered 400 MEMBER_NOT_FOUND before. From the vendor handler (item 3), the old route answered about the active organisation: a caller who was a member there got a 200 with the active row — the silent wrong answer — and 400 MEMBER_NOT_FOUND fired only when the caller also had no row in the active organisation. The PR's own ablation agrees (case ⑤ went red as "expected undefined to be 'YOU_ARE_NOT_A_MEMBER…'", i.e. the old shape resolved, it did not throw). The text originates in the prior review's F3 and the ruling that quoted it, so the dev implemented what was ruled; the "after" (403) is right, and bullet :22 already states the true before-state for every non-active id. Expectation: one-line correction in both places ("Before: a 200 carrying the active organisation's row, or 400 MEMBER_NOT_FOUND when the caller had no row there either") — travels with whatever patch posts the F1 report; not a landing blocker.
F3 — non-blocking (informational): the falsy-id refusal is a plain Error. index.ts:3484 throws without code/httpStatus, unlike the server refusals the method surfaces in the ADR-0112 envelope. This matches the review's own F2 expectation and the pre-existing empty-page throw at :3509-3511; a caller branching on err.code sees undefined for this one input. No action in this PR.
F4 — non-blocking (informational): organizations.getActiveMember is pinned by URL and value, no longer by name. No client: field in the ledger names it (the notes do, but auth-route-ledger-coverage.test.ts:57 resolves client: only). The URL conformance sweep and cases ①–⑧ carry it. Same residual the prior review recorded; no action.
F5 — non-blocking (disclosure to the maintainer): the ADR-0087 exemption rests on before/after phrasing. The ruling asked for "FROM→TO lines"; they are delivered as Before/After observations (:22-25) with one hint ("auth.me() is where that id is readable"), per execution note 5594607180, whose veto window was not exercised. The category is right on substance (no metadata conversion), so this is a disclosure, not a defect.
CI at read time
Head 4ebf8692d9c5cfb896c6d4d03c50a6af289b2278: 37 check runs, 33 latest-per-name: 28 success, 5 skipped, 0 failure, 0 in progress. Skipped (all label-gated or opt-in): Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in). Green include Check Changeset (03:29:15Z — hosts the ADR-0087 and no-major steps), Lint & Repo Gates, all four Type Check · lanes + aggregator, Test Core 6/6 + aggregator, Dogfood Regression Gate 3/3, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, both single-writer/issue-claim guards. PR is draft with needs:contract-review; card carries needs:contract-review, pm:queue, no assignee.
Implemented-by: branch claude/issue-16568-get-active-member-organization-id
Reviewed-by: director seat summon #20 (isolated fable subagent, transcript-verified before adoption)
{"pr":16761,"head":"4ebf8692d9c5cfb896c6d4d03c50a6af289b2278","verdict":"PASS WITH FINDINGS","blocking":[],"clause2":"yes","semver_ok":true,"governed":false,"ci":"33 latest-per-name: 28 success, 5 skipped (Auto Label, Build Docs, Check PR Size, Console Pin Gate, Packed-tarball smoke), 0 failure, 0 in_progress"}
Generated by Claude Code
…he named organisation The changeset bullet and the `getActiveMember` docblock both said a caller who was not a member of the NAMED organisation used to draw `400 MEMBER_NOT_FOUND`. better-auth 1.7.2's `get-active-member` handler reads `session.session.activeOrganizationId` and never `ctx.query`, so the named organisation was never consulted at all: such a caller drew a 200 carrying the ACTIVE organisation's row, and `MEMBER_NOT_FOUND` fired only when the caller had no row in the active organisation either. The PR's own ablation agrees — case ⑤ went red as "expected undefined to be 'YOU_ARE_NOT_A_MEMBER…'", i.e. the old shape resolved rather than throwing. Both sentences now state that before-state. The `after` (403) was already right, and the neighbouring bullets already stated it for every other input. Prose only: the changeset body ships as CHANGELOG text and the docblock is a comment. No executable line, no test and no behaviour moves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Js5kTpTtxieBjPyScgxJ3
Landing note — the standing PASS WITH FINDINGS (5597568086, head
|
|
Enqueue record — director seat, summon #18 segment 3 (
Generated by Claude Code |
Fixes #16568
Clause-②: yes
The defect
organizations.getActiveMember(organizationId)builtGET /organization/get-active-member?organizationId=…. better-auth 1.7.2's handler for that path (plugins/organization/routes/crud-members.mjs) readssession.session.activeOrganizationIdand never looks atctx.query, so the query string was dead on arrival: a client doing a permission check for organisation B while A was active was told about A, at 200, with no diagnostic. The SDK's own JSDoc promised "the calling user's membership row in the given organisation" — a declared capability the runtime did not deliver.Zone 1's hard precondition, measured BEFORE any implementation
Triage recommended
list-membersbut said in writing it had not verified the query shape. It was driven first: a realAuthManager(better-auth 1.7.2, organization plugin, teams enabled) over a realSqlDriver(better-sqlite3:memory:), one user owning two organisations with A active, plus a second member seeded into B so the filter has something to exclude. Transcript, trimmed to the ids that matter:RA/RB/RC are the discriminating leg: with two rows in B, the self filter returns exactly one and the other-user filter returns the other, so
filterField=userIdreally narrows rather than merely not breaking. R3/R4 are the addressing leg. The precondition holds, so option 2 was implemented; nothing was improvised and the decision inbox was not needed.The vendor premise was re-confirmed on the same drive: the installed version is exactly
better-auth 1.7.2(pinned by PR #16634), and itsgetActiveMemberhandler still reads session state only. The card's premise stands.What changed
packages/client/src/index.ts,organizations.getActiveMember— the signature and the declared return type are byte-identical; only the addressing moved:GET /get-sessionfor the caller's own user id (bare{ user, session }for a signed-in caller, the literalnullfor an anonymous one — measured);GET /organization/list-members?organizationId=…&filterField=userId&filterValue=SELF_USER_ID&limit=1, unwrapping the one-entry page.list-membersrows carry the identical shape —{id, organizationId, userId, role, createdAt, user:{id,name,email,image}}— which is whyOrganizationMemberWithUserWiredoes not move.The JSDoc is corrected in the same stroke, as triage required. #14314's PR had changed it to say the argument is ignored; that sentence is now false, so it is replaced by what the method does, plus every behaviour an existing caller can observe change.
Does the request-byte change constitute a published behaviour change? Yes — declared, not argued away
Triage asked for this in writing, so here it is, item by item. The request bytes change, and so does the answer:
403 YOU_ARE_NOT_A_MEMBER_OF_THIS_ORGANIZATIONwhere the old shape produced400 MEMBER_NOT_FOUND— and about a different organisation at that, since the old shape never asked about the named one. Two published error codes, and the input class that reaches each of them is re-chosen;400 NO_ACTIVE_ORGANIZATION.setActivehas stopped being a precondition;401 UNAUTHORIZED, thrown by the same session middleware that guarded the old route. Nothing client-side is substituted for the server's refusal;Clause-②: yes — re-declared from the delivered diff
The dispatch carried a
noas triage's reading, marked explicitly as not measured. Re-declared here, and it flips. The machine-read declaration is the standalone line at the top of this body, in the fixed spelling — this heading and the paragraphs under it are the argument, not the declaration.The mechanical floor is clean: no new exported symbol, no new key on a published payload, no signature change, no type change (
check:exported-any-returnsis untouched,check:dts-closureandcheck:type-source-resolutionboth green). But the floor is not the whole test, and the contract-review rule names this exact case as one that needs judgement rather than a mechanism: "在两个已发布码之间重选输入类". That is precisely what the second bullet above is — the input class that produces each of two published ADR-0112 codes is re-chosen — and the answer to which row an existing caller receives changes with it. Under "claim 拿不准 ⇒ 按yes" that is ayestwice over.needs:contract-reviewis hung on this PR at creation, and on the card, as the double carrier requires.Reverse verification
The fix was committed first, then the pre-fix
packages/client/src/index.tswas restored for one run.`organization/list-members`counted 2 before and 1 after (the surviving one is a pre-existing JSDoc occurrence at line 1358 — the printed "expect 0" label in the ablation script was wrong about that constant, the observation was not), and the blob hash movedef5fa760…to7fa9e129…;expected 'org_alpha' to be 'org_bravo'), ② the request bytes, ④ no-active-organisation, ⑤ the 403 envelope (expected undefined to be 'YOU_ARE_NOT_A_MEMBER…'), ⑥ the anonymous 401 request count. ③ (naming the active organisation) and ⑦ (the guard-the-guard leg that drives the dead route directly) stay green, as predicted — ③ is the one case the old shape got right by coincidence;git checkout HEAD -- …, thengit diff HEADempty and the on-disk blob hash back toef5fa760…, byte for byte. The script carried atrap … EXIT INT TERMwith absolute paths throughout.No
distis in the resolution path here: the suite imports./indexrelatively, i.e. the source in this checkout, so the ablation could not have been read against a stale build.Tests
New:
packages/client/src/organization-get-active-member-addressing.test.ts, 7 cases. Its fixture is not an approximation — every status, code and row shape in it is a transcript line from the drive above, and it keeps the defect alive onget-active-member(that arm still answers the active organisation whatever the query names), so a regression to the old route fails on the row value rather than on a URL string.pnpm --filter @objectstack/client testpnpm --filter @objectstack/client typechecktsc --noEmit+check:test-typecheck(0 files / 0 errors in the debt ledger)pnpm --filter @objectstack/plugin-auth testpnpm --filter @objectstack/plugin-auth typecheckpnpm --filter '@objectstack/client-react...' buildGates
Derived from the delivered diff with
node scripts/pm/dispatch-gates.mjs --commands, from a tree actually atorigin/main(noSTALE TREEbanner —origin/mainhad moved twice during the round and was merged in first), and reconciled:All 59 exit 0, each captured before any pipe. Three needed a second lap and none of the three is a NOT MEASURED in the final record:
pnpm check:doc-authoringwas genuinely red on this diff: the new ledgernotecarried#16568in a runtime string, against the maintainer's ruling 「处理 issue 时犯的错应该总结成经验,保留 issue id没有意义」. The id is stripped; git history keeps the anchor. Green.pnpm --filter @objectstack/spec run check:skill-examplesandpnpm check:dual-build-cjs-loadsboth refused for want of built output (the second by its ownexit 3PREREQUISITE NOT MET). Both green after theclient-reactclosure build — 258 prose examples type-check across 3 surfaces.pnpm check:type-check-debtOOM-ed at--max-old-space-size=4096and answeredexit 3, its own PREREQUISITE-NOT-MET code. Re-run at 8192 (the gate itself runs tsc under a CI-shaped 6144 ceiling, so 4096 could never have held the wrapper): green, 5 ledger entries re-measured, none above its recorded number.Lint is the full repo-wide union, not a narrowing:
eslint . --no-inline-config --format jsonat012d430b— 6347 files, 0 errors, 0 warnings, exit 0.Declared scope extension: one ledger row outside the dispatched file surface
The dispatch named
packages/client/src/index.tsplus a test underpackages/client/. This PR also edits one row ofpackages/plugins/plugin-auth/src/auth-route-ledger.ts, and that is deliberate rather than drift:disposition: 'sdk'means "expressed by the SDK", and after this change no SDK method builds that URL, so leaving the row would ship a false statement in a truth ledger created by this diff. It is rebookedserver-onlywith the rationale the hygiene test demands,clientdropped, modelled on the neighbouringorganization/add-memberrow which carries exactly this shape.The bounded in-place exemption's four conditions, each checked rather than asserted: (i) same defect class as the card — a declared capability the runtime does not deliver; (ii) mechanical, with the target shape already pinned by
AuthRouteDispositionand the hygiene case that demands anoteon every non-sdkrow; (iii) zero holders — scanned per-ref against each open PR's own merge-base, positive control fired; (iv) same gate family, no new validation surface (auth-route-ledger.conformance.test.ts,auth-route-ledger-coverage.test.tsandpnpm check:auth-mount-ledgeralready read this file, and all three are green).Nothing published moves with it: the module has zero runtime importers in non-test source, and
tsupbuilds onlysrc/index.tsandsrc/rate-limit-storage.ts, so it cannot reachdist. Hence one changeset, for@objectstack/clientalone.Serial
packages/client/src/index.tsis the #12104 family's hard-serial hot file. Re-measured at claim time rather than inherited: zero holders across 11 of 11 open PRs, per-ref against each PR's own merge-base, with two positive controls firing (packages/cli/src/commands/validate.tsin #16727,packages/client/package.jsonin #15334). The same scan found zero holders onauth-route-ledger.ts.验收备注
getActiveMemberis now a slightly odd name for a method that takes an organisation id and no longer touches the "active" anything. Renaming is a published-surface decision and pure naming, so it is not filed and not done here — the signature was held fixed by the ruling on purpose, so callers need no edit.get-active-membercannot address an organisation, andget-active-member-roleanswers a role only. The SDK caches no session, so the caller's own id has to be read. Noted, not filed — it is a cost of the honest shape, not a defect.auth.me/auth.refreshTokendeclare the REST{ success, data }envelope for/get-session, which answers the bare{ user, session }— andrefreshTokennever captures a token because of it #16760 (out of scope, class (b), found on this drive):auth.meandauth.refreshTokenboth declareSessionResponse— the REST{ success, data }envelope — for/get-session, which answers the bare{ user, session }(and the literalnullwhen anonymous). It is not a type-only complaint:refreshTokenacts on that annotation, readingdata.data?.token, so it never captures a token and returns successfully anyway. Distinct from client SDKauth.*family: bind the 14return res.json()methods (auth 7 · sessions 3 · twoFactor 3 · accounts.unlink 1) to their better-auth wire shapes — #12104 family card 2 of 3 #14313, whose enumerated set is the methods carrying no annotation; same lane and same hot file, so scheduling them together would be natural.search_issues— a declared channel switch, because the proxy refuses/search/*by design (403,sessions are bound to their configured repositories) and REST list endpoints cannot match body text. It returned four related cards including client SDKauth.*family: bind the 14return res.json()methods (auth 7 · sessions 3 · twoFactor 3 · accounts.unlink 1) to their better-auth wire shapes — #12104 family card 2 of 3 #14313 and client SDKorganizations.*family: bind the 19return res.json()methods (organizations 11 · invitations 3 · teams 5) to their better-auth wire shapes — #12104 family card 3 of 3 #14314's neighbours, so the channel was live and the result is a reading rather than a silent zero.Generated by Claude Code